package org.apache.ocean.main.name;

import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.sourceforge.jsorter.ReflectColumns;
import net.sourceforge.jsorter.SortReflect;
import net.sourceforge.jsorter.Sorter;
import net.sourceforge.jsorter.SorterConstants;
import net.sourceforge.jsorter.SortReflect.MethodException;

import org.apache.ocean.main.IndexSchema;
import org.apache.ocean.main.client.OceanXML;
import org.apache.ocean.main.http.HttpParameters;
import org.apache.ocean.main.http.HttpRequest;
import org.apache.ocean.main.http.HttpTimeout;
import org.apache.ocean.main.http.MulticastHttp;
import org.apache.ocean.main.http.MulticastHttp.ExceptionEnum;
import org.apache.ocean.main.util.FileLoader;
import org.apache.ocean.main.util.RefWaitNotNull;
import org.apache.ocean.main.util.RestartableTimer;
import org.apache.ocean.main.util.Timeout;
import org.apache.ocean.main.util.RestartableTimer.TimeoutRunnable;
import org.apache.ocean.util.ConcurrentTreeMap;

/**
 * maitains which name is master
 */
// TODO: implement file client
public class NameClient implements IndexSchema.Loader {
	public static Logger log = Logger.getLogger(NameClient.class.getName());
	private List<URL> urls;
	private RefWaitNotNull<Name> masterRef;
	private ExecutorService threadPool;
	private Map<String, Name> nameMap;
	private RestartableTimer masterPingTimer;
	private Listener listener;
	private ConcurrentTreeMap<Long,IndexSchema> indexSchemaMap = new ConcurrentTreeMap<Long,IndexSchema>();

	public NameClient(List<URL> urls, Listener listener, ExecutorService threadPool) throws Exception {
		this.urls = urls;
		this.listener = listener;
		this.threadPool = threadPool;
		masterPingTimer = new RestartableTimer("keepalive", new MasterPing(), 10 * 1000, 4000);
	}
  
	public class FileLoaderImpl implements FileLoader { 
		
	}
	
	public static interface Listener {
		public void newMaster(Name master);
	}
	
	public HttpTimeout getHttpTimeout() {
		return new HttpTimeout(1000, 1000);
	}
	
	public IndexSchema getIndexSchema(Timeout timeout) throws Exception {
		IndexSchema indexSchema = indexSchemaMap.lastValue();
		if (indexSchema == null) {
			indexSchema = loadIndexSchema(new HttpParameters("action", "getlatestindexschema"), timeout);
		}
		return indexSchema;
	}
	
	public IndexSchema getIndexSchema(long version, Timeout timeout) throws Exception {
		IndexSchema indexSchema = indexSchemaMap.get(version);
		if (indexSchema == null) {
			String name = "schema."+version+".xml";
			HttpParameters parameters = new HttpParameters();
			parameters.add("action", "getfile");
			parameters.add("name", name);
			indexSchema = loadIndexSchema(parameters, timeout);
			assert indexSchema.getVersion() == version;
			indexSchemaMap.put(indexSchema.getVersion(), indexSchema);
		}
		return indexSchema;
	}
	
	private IndexSchema loadIndexSchema(HttpParameters parameters, Timeout timeout) throws Exception {
		URL url = getMaster(timeout).getUrl();
		HttpTimeout httpTimeout = new HttpTimeout(1000, (int)timeout.getTimeLeft());
		String xml = HttpRequest.get(url, parameters, httpTimeout).getBodyString();
		return new IndexSchema(xml);
	}
	
	public class MasterPing implements TimeoutRunnable {
		public void run(Timeout timeout) {
			try {
				while (timeout.isOK()) {
					Name master = null;
					if (masterRef.isNull()) {
						master = getAgreedMaster();
					}
					if (master == null) {
						NameClient.this.masterRef.set(null);
					} else {
						NameClient.this.masterRef.set(master);
						if (NameClient.this.masterRef.isNotNull()) {
							if (!NameClient.this.masterRef.getNoWait().equals(master.getId())) {
								notifyNewMaster(master);
							}
						}
						break;
					}
					Thread.sleep(100);
				}
			} catch (Exception exception) {
				NameClient.this.masterRef.set(null);
				log.log(Level.SEVERE, exception.getMessage());
			}
		}
	}

	public void notifyNewMaster(Name master) throws InterruptedException {
    if (listener != null) listener.newMaster(master);
	}

	public Map<String, Name> getNames() {
		return new HashMap(nameMap);
	}

	public List<Name> getNames(String action) throws Exception {
		MulticastHttp multicastHttp = new MulticastHttp(threadPool, ExceptionEnum.NONE);
		HttpParameters httpParameters = new HttpParameters("action", action);
		List<OceanXML> responses = multicastHttp.execute(urls, httpParameters, getHttpTimeout());
		List<Name> names = new ArrayList<Name>();
		for (OceanXML response : responses) {
			names.add(new Name(response.getFirstInBody()));
		}
		return names;
	}
  
	public Name getMaster(Timeout timeout) throws Exception {
		if (masterRef.isNull()) {
			masterPingTimer.runNow();
		}
		return masterRef.get(timeout);
	}
	
	public Name getMasterNoWait() throws Exception {
		if (masterRef.isNotNull())
			return masterRef.get();
		if (masterRef.isNull()) {
			masterPingTimer.runNow();
		}
		return null;
	}
  
	private Name getAgreedMaster() throws Exception {
		List<Name> names = getNames("getchosenmaster");
		Map<String, Name> nameMap = new HashMap<String, Name>();
		for (Name name : names) {
			nameMap.put(name.getId(), name);
		}
		if (nameMap.size() == 1)
			return nameMap.values().iterator().next();
		return null;
	}
	
	public Name getChosenMaster() throws Exception {
		List<Name> names = getNames("getname");
		try {
			ReflectColumns reflectColumns = new ReflectColumns();
			reflectColumns.add("replicationId", SortReflect.method("getReplicationId"), SorterConstants.DESCENDING_ORDER);
			reflectColumns.add("id", SortReflect.method("getId"), SorterConstants.ASCENDING_ORDER);
			Sorter sorter = Sorter.createReflect(reflectColumns, names, SorterConstants.NULLS_ARE_LEAST);
			List<Name> sortedNames = sorter.sortReflect();
			Name chosenMaster = sortedNames.get(0);
			return chosenMaster;
		} catch (MethodException methodException) {
			throw new RuntimeException(methodException.getCause());
		}
	}
}
